package project.auth;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Objects;

import project.common.StringUtil;

import common.db.JdbcSource;
import common.db.jdbc.Jdbc;

import core.exception.PhysicalException;
import core.exception.ThrowableUtil;
import core.util.CodecUtil;
import core.util.DateUtil;

/**
 * 認証クラス
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class AuthenticationImpl implements Authentication {

	/** カラム名（ログオンセッション） */
	private static final String SK_COL_LOGON_SESS = "LOGON_SESS_PRM";
	/** カラム名（最終処理時間） */
	private static final String SK_COL_SAISHU_DTM = "SAISHU_SHORI_DTM";
	/** カラム名（チケットパラメタ） */
	private static final String SK_COL_TICKET = "TICKET_PRM";
	/** カラム名（ログオン時間） */
	private static final String SK_COL_LOGON_DTM = "LOGON_DTM";
	/** カラム名（ログオフ時間） */
	private static final String SK_COL_LOGOFF_DTM = "LOGOFF_DTM";

	/** 検索SQL */
	private static final String SK_SEL_SQL =
						"SELECT USER_ID, LOGON_SESS_PRM, "
						+ "SAISHU_SHORI_DTM, TICKET_PRM, "
						+ "LOGON_DTM, LOGOFF_DTM, LOGOFF_SBT "
						+ "FROM SESSION_KANRI WHERE USER_ID = ?";
	/** 新規SQL */
	private static final String SK_INS_SQL =
						"INSERT INTO SESSION_KANRI"
						+ " (USER_ID, LOGON_SESS_PRM, TICKET_PRM,"
						+ " SAISHU_SHORI_DTM, LOGON_DTM, LOGOFF_SBT)"
						+ " VALUES (?,?,'',?,?,'')";
	/** 更新SQL */
	private static final String SK_UPD_SQL =
						"UPDATE SESSION_KANRI SET "
						+ "LOGON_SESS_PRM = ?, "
						+ "TICKET_PRM = '', "
						+ "SAISHU_SHORI_DTM = ?, "
						+ "LOGON_DTM = ?, "
						+ "LOGOFF_DTM = ?, "
						+ "LOGOFF_SBT = ? "
						+ "WHERE USER_ID = ?";
	/** 検索SQL（排他） */
	private static final String SK_SEL_SQL_EXCLUSIVE = SK_SEL_SQL + " FOR UPDATE";
	/** 更新SQL2 */
	private static final String SK_UPD_SQL2 =
						"UPDATE SESSION_KANRI SET "
						+ "SAISHU_SHORI_DTM = ? "
						+ "WHERE USER_ID = ? AND LOGON_SESS_PRM = ?";
	/** 更新SQL3 */
	private static final String SK_UPD_SQL3 =
						"UPDATE SESSION_KANRI SET "
						+ "TICKET_PRM = ? WHERE USER_ID = ? ";
	/** パスワードチェックSQL */
	private static final String UI_SEL_SQL =
						"SELECT USER_ID \n"
						+ "FROM M_USER_INFO \n"
						+ "WHERE USER_ID = ? ";
	/** 条件SQL */
	private static final String UI_SEL_WHERE = "AND PASS_WD = ?";

	/**
	 * ログオンチェック
	 *
	 * @param uid ユーザID
	 * @param sid セションID
	 * @return 正常なら true
	 */
	@Override
	public boolean checkLogon(final String uid, final String sid) {
		// セション情報取得
		SessionInfo m = getSessionKanriInfo(uid, false);
		return m != null && m.getLogonSession() != null && m.getShoriDateTime() != null
				&& m.getLogonSession().equals(sid);
	}

	/**
	 * ログオン処理
	 *
	 * @param uid ユーザID
	 * @param pwd パスワード
	 * @return 成功なら true
	 */
	@Override
	public boolean logon(final String uid, final String pwd) {
		return logonProc(uid, pwd) && clearTicket(uid);
	}

	/**
	 * ユーザのステータスを更新/追加
	 *
	 * @param uid ユーザID
	 * @param now 現在日時
	 * @param sid セションID
	 * @param interval 最大無効間隔
	 * @return アクションセションID
	 */
	@Override
	public SessionInfo updateStatus(
			final String uid, final Date now, final String sid, final int interval) {

		Timestamp datetime = DateUtil.toDateTime(now);
		SessionInfo m = getSessionKanriInfo(uid, true);
		try (Connection conn = JdbcSource.getConnection()) {
			if (m == null) {
				try (PreparedStatement upsmt = conn.prepareStatement(SK_INS_SQL)) {
					int i = 1;
					upsmt.setString(i++, uid);
					upsmt.setString(i++, sid);
					upsmt.setTimestamp(i++, datetime);
					upsmt.setTimestamp(i++, datetime);
					upsmt.executeUpdate();
				}
				m = new SessionInfo();
			} else {
				String sess = m.getLogonSession();
				if (sess != null && sess.equals(sid)) {
					return null;
				}

				m.setInterval(interval);
				m.setDateTime(datetime);

				try (PreparedStatement upsmt = conn.prepareStatement(SK_UPD_SQL)) {
					int i = 1;
					upsmt.setString(i++, sid);
					upsmt.setTimestamp(i++, datetime);
					upsmt.setTimestamp(i++, datetime);
					upsmt.setTimestamp(i++, m.getLogoffDateTime());
					upsmt.setInt(i++, m.getLogoffType().value());
					upsmt.setString(i++, uid);
					upsmt.executeUpdate();
				}
			}
			conn.commit();
			return m;

		} catch (SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * ログオフ
	 *
	 * @param uid ユーザID
	 * @param sid セションID
	 * @param time 日時
	 */
	@Override
	public void logoff(final String uid, final String sid, final Timestamp time) {
		// セション情報取得
		SessionInfo m = getSessionKanriInfo(uid, true);
		if (m != null && sid != null && sid.equals(m.getLogonSession())) {
			try (Connection conn = JdbcSource.getConnection()) {
				try (PreparedStatement psmt = conn.prepareStatement(SK_UPD_SQL)) {
					int i = 1;
					psmt.setString(i++, "");
					psmt.setString(i++, "");
					psmt.setTimestamp(i++, m.getShoriDateTime());
					psmt.setTimestamp(i++, m.getLogonDateTime());
					psmt.setTimestamp(i++, time);
					psmt.setInt(i++, m.getLogoffType().value());
					psmt.setString(i++, uid);
					psmt.executeUpdate();
				}
				conn.commit();
			} catch (SQLException ex) {
				ThrowableUtil.error(ex);
				throw new PhysicalException(ex);
			}
		}
	}

	/**
	 * アクション時間を設定する。
	 *
	 * @param uid ユーザID
	 * @param now 現在日時
	 * @param sid セションID
	 */
	@Override
	public void setActionTime(final String uid, final Date now, final String sid) {
		// セション情報取得
		if (getSessionKanriInfo(uid, true) != null) {
			try (Connection conn = JdbcSource.getConnection()) {
				try (PreparedStatement psmt = conn.prepareStatement(SK_UPD_SQL2)) {
					// 更新
					int i = 1;
					psmt.setTimestamp(i++, DateUtil.toDateTime(now));
					psmt.setString(i++, uid);
					psmt.setString(i++, sid);
					psmt.executeUpdate();
				}
				conn.commit();
			} catch (SQLException ex) {
				ThrowableUtil.error(ex);
				throw new PhysicalException(ex);
			}
		}
	}

	/**
	 * チケット追加
	 *
	 * @param uid ユーザID
	 * @return チケット
	 */
	@Override
	public String addTicket(final String uid) {
		// セション情報取得
		SessionInfo m = getSessionKanriInfo(uid, true);
		if (m == null) {
			return null;
		}

		// チケット作成
		String ticket = StringUtil.randomization(10);
		String prm = m.getTicket();
		if (!Objects.toString(prm, "").isEmpty()) {
			prm = prm + "," + ticket;
		} else {
			prm = ticket;
		}

		// 更新
		updateTicket(uid, prm);

		return ticket;
	}

	/**
	 * チケット削除処理
	 *
	 * @param uid ユーザID
	 * @param val チケット
	 * @return 存在していて削除された場合 true 存在しない場合等はfalse
	 */
	@Override
	public boolean removeTicket(final String uid, final String val) {
		// セション情報取得
		SessionInfo m = getSessionKanriInfo(uid, true);
		if (m != null && !Objects.toString(m.getTicket(), "").isEmpty()) {
			String prm = m.getTicket();
			int loc = prm.indexOf(val);
			if (0 <= loc) {
				int len = val.length();
				if (0 < loc && prm.codePointBefore(loc) == ',') {
					loc--;
					len++;
				} else if (loc + len < prm.length()) {
					len++;
				}
				prm = prm.substring(0, loc) + prm.substring(loc + len);
				// 更新
				updateTicket(uid, prm);
				return true;
			}
		}
		return false;
	}

	/**
	 * パスワードチェック
	 *
	 * @param uid ユーザID
	 * @param pwd パスワード
	 * @return 成功なら true
	 */
	private boolean logonProc(final String uid, final String pwd) {
		// サブミット値取得
		if (Objects.toString(uid, "").isEmpty() || Objects.toString(pwd, "").isEmpty()) {
			return false;
		}

		try (Jdbc conn = JdbcSource.getConnection()) {
			// ユーザ存在確認
			try (PreparedStatement psmt1 = conn.readonlyStatement(UI_SEL_SQL)) {
				psmt1.setString(1, uid);

				try (ResultSet rs = psmt1.executeQuery()) {
					if (!rs.next()) {
						return false;
					}
				}
			}

			// パスワード確認
			try (PreparedStatement psmt2 = conn.readonlyStatement(
					UI_SEL_SQL + UI_SEL_WHERE)) {
				psmt2.setString(1, uid);
				psmt2.setString(2, CodecUtil.toMd5String(pwd));

				try (ResultSet rs = psmt2.executeQuery()) {
					return rs.next();
				}
			}

		} catch (SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * チケットクリア
	 *
	 * @param uid ユーザID
	 * @return クリアした場合 true
	 */
	private boolean clearTicket(final String uid) {
		return updateTicket(uid, "");
	}

	/**
	 * チケット更新
	 *
	 * @param uid ユーザID
	 * @param val チケット
	 * @return 更新した場合 true
	 */
	private boolean updateTicket(final String uid, final String val) {
		// セション情報取得
		if (getSessionKanriInfo(uid, true) != null) {
			try (Connection conn = JdbcSource.getConnection()) {
				try (PreparedStatement psmt = conn.prepareStatement(SK_UPD_SQL3)) {
					psmt.setString(1, val);
					psmt.setString(2, uid);

					int cnt = psmt.executeUpdate();
					if (cnt == 1) {
						conn.commit();
						return true;
					}
				}
			} catch (SQLException ex) {
				ThrowableUtil.error(ex);
				throw new PhysicalException(ex);
			}
		}
		return false;
	}

	/**
	 * セション管理情報取得
	 *
	 * @param uid ユーザID
	 * @param lock 排他をかける場合 true
	 * @return セション情報
	 */
	private SessionInfo getSessionKanriInfo(final String uid, final boolean lock) {
		if (!Objects.toString(uid, "").isEmpty()) {
			try (Connection conn = JdbcSource.getConnection()) {
				try (PreparedStatement psmt = getPreparedStatement(conn, lock)) {
					psmt.setString(1, uid);

					try (ResultSet rs = psmt.executeQuery()) {
						if (rs.next()) {
							return new SessionInfo(rs.getString(SK_COL_LOGON_SESS),
										rs.getTimestamp(SK_COL_SAISHU_DTM),
										rs.getString(SK_COL_TICKET),
										rs.getTimestamp(SK_COL_LOGON_DTM),
										rs.getTimestamp(SK_COL_LOGOFF_DTM));
						}
					}
				}
			} catch (SQLException ex) {
				ThrowableUtil.error(ex);
				throw new PhysicalException(ex);
			}
		}
		return null;
	}

	/**
	 * PreparedStatement 取得
	 * @param conn コネクション
	 * @param lock ロックフラグ
	 * @return PreparedStatement
	 * @throws SQLException SQL例外
	 */
	private PreparedStatement getPreparedStatement(
			final Connection conn, final boolean lock) throws SQLException {
		if (lock) {
			return conn.prepareStatement(SK_SEL_SQL_EXCLUSIVE);
		}
		return Jdbc.wrap(conn).readonlyStatement(SK_SEL_SQL);
	}
}
